VariationalAutoencoder (Score: 3.5 / 5.0)

  1. Task (Score: 0.0 / 1.5)
  2. Comment
  3. Coding free-response (Score: 1.5 / 1.5)
  4. Coding free-response (Score: 2.0 / 2.0)
  5. Comment
  6. Coding free-response (Score: 0.0 / 0.0)

Przed oddaniem zadania upewnij się, że wszystko działa poprawnie. Uruchom ponownie kernel (z paska menu: Kernel$\rightarrow$Restart) a następnie wykonaj wszystkie komórki (z paska menu: Cell$\rightarrow$Run All).

Upewnij się, że wypełniłeś wszystkie pola TU WPISZ KOD lub TU WPISZ ODPOWIEDŹ, oraz że podałeś swoje imię i nazwisko poniżej:

In [1]:
NAME = "Michal Marciniak"

Autokodery wariacyjne (Variational Autoencoders)

Autokoder

Autokoder (ang. autoencoder) to model trenowany w zadaniu rekonstruowania wejścia. Zazwyczaj składa się z dwóch sieci neuronowych:

  • kodera (funkcji $f(\mathbf{x})$ - kodujÄ…cego wejÅ›cie do postaci ukrytej (latent) $\mathbf{h} = f(\mathbf{x})$,
  • dekodera (funkcji $g(\mathbf{h})$ - rekonstruujÄ…cego wejÅ›cie $\mathbf{r} = g(\mathbf{h})$.

Innymi słowy, autokoder jest modelem mapującym wejście $\mathbf{x}$ na wyjście $\mathbf{r}$ poprzez jego wewnętrzną, ukrytą reprezentację $\mathbf{h}$.

image.png

Głównym celem trenowania modelu jest znalezienie najlepszej pary koder-dekoder, która zachowuje maksimum informacji podczas kodowania, co daje najmniejszy błąd rekonstrukcji $\mathcal{L}$:

$$(f^*, g^*) = \arg\min\mathcal{L}(\mathbf{x}, g(f(\mathbf{x}))).$$

W przypadku autokoderów często używaną funkcją kosztu jest błąd średniokwadratowy.

Klasyczne autokodery używane są w zwykle w celu redukcji wymiarowości lub wstępnego uczenia cech do modelu. Ze względu na brak wykorzystania etykiet, modele te są trenowane w sposób nienadzorowany.

Autokoder wariacyjny

Wykorzystanie wyłącznie błędu rekonstrukcji jako funkcji celu w klasycznych autokoderach wymusza na modelu uczenie się skompresowanej reprezentacji danych, jednak często może prowadzić do jego przetrenowania, przez co jego zdolności generatywne są ograniczone (głównie ze względu na nieregularną przestrzeń ukrytą).

Rozwiązaniem pozwalającym na wyuczenie się reprezentacji o wyższej jakości są autokodery wariacyjne (ang. variational autoencoders). Jest to model generatywny, gdzie zamiast uczenia funkcji kodera mapującej wejście do przestrzeni ukrytej będziemy próbowali uczyć się nieznanego rozkładu danych $p_{\theta^*}(\mathbf{z})$.

image-2.png

Skupmy się na modelu generatywnym $p_\theta(\mathbf{z})p_\theta(\mathbf{x} | \mathbf{z})$, oznaczonym liniami ciągłymi. Zakładamy, że przykłady ze zbioru danych $\mathbf{X} = \left\{x^{(i)}\right\}_{i=1}^N$, składającego się z $N$ niezależnych i pochodzących z tego samego rozkładu przykładów, generowane są przez proces losowy, w którym występuje nieobserwowana, ciągła zmienna losowa $\mathbf{z}$. Proces ten składa się z dwóch kroków:

  1. wektor ukryty $\mathbf{z}^{(i)}$ generowany jest z rozkładu a priori $p_{\theta^*}(\mathbf{z})$,
  2. obserwacja $\mathbf{x}^{(i)}$ jest generowana z rozkładu warunkowego $p_{\theta^*}(\mathbf{x} | \mathbf{z})$.

Zakładamy tutaj, że rozkłady $p_{\theta^*}(\mathbf{z})$ oraz $p_{\theta^*}(\mathbf{x} | \mathbf{z})$ należą do rodzin rozkładów $p_{\theta}(\mathbf{z})$ oraz $p_{\theta}(\mathbf{x} | \mathbf{z})$, parametryzowanych przez $\theta$; zakładamy że ich funkcje gęstości są różniczkowalne względem $\theta$ i $\mathbf{z}$. Zależność $\mathbf{x}^{(i)}$ od $\mathbf{z}^{(i)}$ będziemy modelować przy użyciu sieci neuronowej o parametrach $\theta$.

Parametry te moglibyśmy znaleźć maksymalizując likelihood:

$$p(\mathbf{x}) = \int p_\theta(\mathbf{x} | \mathbf{z})p(\mathbf{z})d\mathbf{z}.$$

Nie jest to jednak możliwe, ze względu na całkowanie po wszystkich wartościach priora. Wprowadzimy zatem model probabilistycznego kodera $q_\phi(\mathbf{z} | \mathbf{x})$ (oznaczony linią przerywaną) - aproksymację prawdziwego posteriora $p_\theta(\mathbf{z} | \mathbf{x})$ - rozkład wariacyjny, najczęściej normalny. Będziemy go modelować przy użyciu sieci neuronowej o parametrach $\theta$. W tym kontekście, model generatywny $p_{\theta^*}(\mathbf{x} | \mathbf{z})$ możemy traktować jako probabilistyczny dekoder.

Możemy zatem sformułować naszą funkcję celu w następujący sposób:

$$ \begin{align} \log p_\theta\left(\mathbf{x}\right) & = \mathbb{E}_{\mathbf{z} \sim q_\phi\left(\mathbf{z} | \mathbf{x}\right)} \left[\log p_\theta\left(\mathbf{x}\right)\right] & \text{$p_\theta\left(\mathbf{x}\right)$ jest niezależne od $\mathbf{z}$}\\ & = \mathbb{E}_{\mathbf{z}}\left[\log\frac{p_\theta\left(\mathbf{x} | \mathbf{z}\right)p\left(\mathbf{z}\right)}{p\left(\mathbf{z} | \mathbf{x}\right)}\right] & \text{Reguła Bayesa}\\ & = \mathbb{E}_{\mathbf{z}}\left[\log\frac{p_\theta\left(\mathbf{x} | \mathbf{z}\right)p\left(\mathbf{z}\right)}{p\left(\mathbf{z} | \mathbf{x}\right)}\frac{q_\phi\left(\mathbf{z} | \mathbf{x}\right)}{q_\phi\left(\mathbf{z} | \mathbf{x}\right)}\right] & \text{pomnożyć przez 1}\\ & = \mathbb{E}_{\mathbf{z}}\left[\log p_\theta\left(\mathbf{x} | \mathbf{z}\right)\right] - \mathbb{E}_{\mathbf{z}}\left[\log \frac{q_\phi\left(\mathbf{z} | \mathbf{x}\right)}{p\left(\mathbf{z}\right)}\right] + \mathbb{E}_{\mathbf{z}}\left[\log \frac{q_\phi\left(\mathbf{z} | \mathbf{x}\right)}{p\left(\mathbf{z} | \mathbf{x}\right)}\right] & \text{logarytm}\\ & = \underbrace{\mathbb{E}_{\mathbf{z}}\left[\log p_\theta\left(\mathbf{x} | \mathbf{z}\right)\right] - D_{KL}\left(q_\phi\left(\mathbf{z} | \mathbf{x}\right)\| p\left(\mathbf{z}\right)\right)}_{\mathcal{L}\left(\mathbf{x}, \theta, \phi\right)} + \underbrace{D_{KL}\left(q_\phi\left(\mathbf{z} | \mathbf{x}\right)\| p\left(\mathbf{z} | \mathbf{x}\right)\right)}_{\ge 0}\\ \end{align} $$

Dywergencji Kulbacka-Leiblera między prawdziwym posteriorem i jego aproksymacją nie możemy aproksymować wprost, wiemy jednak że jest zawsze większa lub równa zero. Z tą wiedzą możemy przekształcić to równanie do postaci nierówności i uzyskać funkcję kosztu ELBO:

$$\\log p_\theta \left(\mathbf{x}\right) \ge \underbrace{\mathbb{E}_{\mathbf{z}}\left[\log p_\theta\left(\mathbf{x} | \mathbf{z}\right)\right]}_{\text{błąd rekonstrukcji}} - \underbrace{D_{KL}\left(q_\phi\left(\mathbf{z} | \mathbf{x}\right)\| p\left(\mathbf{z}\right)\right)}_{\text{regularyzacja aproksymacji posteriora}} = \mathcal{L}\left(\mathbf{x}, \theta, \phi\right).$$

Musimy zatem jeszcze przyjąć rozkład prior - najczęściej przyjmuje się rozkład standardowy $\mathcal{N}(0, 1)$.

Model ten uczony będzie metodą Maximum Likelihood Estimation:

$$\theta^*, \phi^* = \underset{\theta, \phi}{\arg\max} \sum_{i=1}^N \mathcal{L}\left(x_i, \theta, \phi\right).$$

Zaczniemy od implementacji klasycznego autokodera z wykorzystaniem biblioteki PyTorch. Będziemy się opierać na klasie bazowej BaseAutoencoder, której metody należy zaimplementować aby możliwe było wykorzystanie przygotowanych analiz.

In [2]:
from typing import Tuple

import matplotlib.pyplot as plt
import numpy as np
import pyro
import pyro.distributions as dist
import torch
import torch.nn as nn
from IPython.display import Code, display
from torch.utils.data import DataLoader
from torchvision.datasets import MNIST
from torchvision.transforms import ToTensor
from tqdm.notebook import tqdm

from src.ae import BaseAutoEncoder
from src.utils import train_ae, AutoEncoderAnalyzer


np.random.seed(2021)
torch.manual_seed(2021)

display(Code(filename="src/ae.py"))
import torch
import torch.nn as nn


class BaseAutoEncoder(nn.Module):
    """Base AutoEncoder module class."""

    def __init__(self, encoder: nn.Module, decoder: nn.Module, n_latent_features: int):
        """
        :param encoder: encoder network
        :param decoder: decoder network
        :param n_latent_features: number of latent features in the AE
        """
        super().__init__()

        self.n_latent_features: int = n_latent_features

        self.encoder: nn.Module = encoder
        self.decoder: nn.Module = decoder

    def forward(self, x: torch.Tensor) -> torch.Tensor:
        """Forward function for mapping input to output."""
        z = self.encoder_forward(x)
        return self.decoder_forward(z)

    def encoder_forward(self, x: torch.Tensor) -> torch.Tensor:
        """ Function to perform forward pass through encoder network.

        takes: tensor of shape [batch_size x input_flattened_size] (flattened input)
        returns: tensor of shape [batch_size x latent_feature_size] (latent vector)
        """
        raise NotImplementedError()

    def decoder_forward(self, z: torch.Tensor) -> torch.Tensor:
        """ Function to perform forward pass through decoder network.

        takes: tensor of shape [batch_size x latent_feature_size] (latent vector)
        returns: tensor of shape [batch_size x output_flattened_size] (flettened output)
        """
        raise NotImplementedError()
        

Tworzymy model kodera i dekodera: oba zawierajÄ… po jednej warstwie ukrytej oraz odpowiednie funkcje aktywacji.

In [3]:
class Encoder(nn.Module):
    """Encoder module; function h."""

    def __init__(
        self,
        n_input_features: int,
        n_hidden_neurons: int,
        n_latent_features: int,
    ):
        """
        :param n_input_features: number of input features (28 x 28 = 784 for MNIST)
        :param n_hidden_neurons: number of neurons in hidden FC layer
        :param n_latent_features: size of the latent vector
        """
        super().__init__()

        self.input_to_hidden = nn.Linear(n_input_features, n_hidden_neurons)
        self.hidden_to_latent = nn.Linear(n_hidden_neurons, n_latent_features)

    def forward(self, x: torch.Tensor) -> torch.Tensor:
        """Encoder forward function."""
        h = self.input_to_hidden(x)
        h = nn.functional.relu(h)
        h = self.hidden_to_latent(h)
        return h


class Decoder(nn.Module):
    """Decoder module; function g."""

    def __init__(
        self,
        n_latent_features: int,
        n_hidden_neurons: int,
        n_output_features: int,
    ):
        """
        :param n_latent_features: number of latent features (same as in Encoder)
        :param n_hidden_neurons: number of neurons in hidden FC layer
        :param n_output_features: size of the output vector (28 x 28 = 784 for MNIST)
        """
        super().__init__()

        self.latent_to_hidden = nn.Linear(n_latent_features, n_hidden_neurons)
        self.hidden_to_output = nn.Linear(n_hidden_neurons, n_output_features)

    def forward(self, h: torch.Tensor) -> torch.Tensor:
        """Decoder forward function."""
        r = self.latent_to_hidden(h)
        r = nn.functional.relu(r)
        r = self.hidden_to_output(r)
        r = torch.sigmoid(r)
        return r

Modele te wykorzystujemy do zaimplementowania Autokodera; implementujemy metody encoder_forward oraz decoder_forward, które służą do tworzenia ukrytej reprezentacji oraz rekonstruowania na jej podstawie obrazu wejściowego.

In [4]:
class Autoencoder(BaseAutoEncoder):
    """Auto encoder module."""

    def __init__(
        self,
        n_data_features: int,
        n_encoder_hidden_features: int,
        n_decoder_hidden_features: int,
        n_latent_features: int,
    ):
        """
        :param n_data_features: number of input and output features (28 x 28 = 784 for MNIST)
        :param n_encoder_hidden_features: number of neurons in encoder's hidden layer
        :param n_decoder_hidden_features: number of neurons in decoder's hidden layer
        :param n_latent_features: number of latent features
        """
        encoder = Encoder(
            n_input_features=n_data_features,
            n_hidden_neurons=n_encoder_hidden_features,
            n_latent_features=n_latent_features,
        )
        decoder = Decoder(
            n_latent_features=n_latent_features,
            n_hidden_neurons=n_decoder_hidden_features,
            n_output_features=n_data_features,
        )
        super().__init__(
            encoder=encoder, decoder=decoder, n_latent_features=n_latent_features
        )
        self.input_shape = None

    def encoder_forward(self, x: torch.Tensor) -> torch.Tensor:
        """Function to perform forward pass through encoder network."""
        if self.input_shape is None:
            self.input_shape = x.shape[1:]
        x = x.view(x.shape[0], -1)
        return self.encoder(x)

    def decoder_forward(self, x: torch.Tensor) -> torch.Tensor:
        """Function to perform forward pass through decoder network."""
        return self.decoder(x).view(-1, *self.input_shape)

W zadaniu ponownie wykorzystamy zbiór MNIST, zawierający odręcznie zapisane cyfry w formie obrazów o rozdzielczości $28\times28$, z wartościami w przedziale $[0, 255]$ (funkcja ToTensor() przetransformuje je do zakresu $[0, 1]$). Zbiór treningowy ograniczamy do 10000 przykładów.

In [5]:
train_dataset = MNIST(root="data", download=True, train=True, transform=ToTensor())
val_dataset = MNIST(root="data", download=True, train=False, transform=ToTensor())

# limiting the dataset
indices = np.random.permutation(len(train_dataset.data))[:10_000]
train_dataset.data = train_dataset.data[indices]
train_dataset.targets = train_dataset.targets[indices]

Definiujemy model wraz z arbitralnie dobranymi hiperparametrami oraz funkcję kosztu (MSE). Następnie wywołujemy przygotowaną pętlę uczenia w celu wytrenowania modelu. Parametry dobrane tutaj powinny spowodować przetrenowanie autokodera.

In [18]:
batch_size = 32
lr = 1e-2
epochs = 20

ae_model = Autoencoder(
    n_data_features=28 * 28,  # MNIST pixels
    n_encoder_hidden_features=128,  # chosen arbitrarily
    n_decoder_hidden_features=128,  # chosen arbitrarily
    n_latent_features=10,  # how many features will be used to represent input
)

train_dataloader = DataLoader(
    train_dataset,
    batch_size=batch_size,
    shuffle=True,
    drop_last=True,
)
val_loader = DataLoader(
    val_dataset, batch_size=batch_size, shuffle=False, drop_last=False
)

loss_fn = nn.MSELoss()

train_ae(
    ae_model,
    epochs=epochs,
    train_loader=train_dataloader,
    val_loader=val_loader,
    lr=lr,
    loss_fn=loss_fn,
)

Out[18]:
Autoencoder(
  (encoder): Encoder(
    (input_to_hidden): Linear(in_features=784, out_features=128, bias=True)
    (hidden_to_latent): Linear(in_features=128, out_features=10, bias=True)
  )
  (decoder): Decoder(
    (latent_to_hidden): Linear(in_features=10, out_features=128, bias=True)
    (hidden_to_output): Linear(in_features=128, out_features=784, bias=True)
  )
)

Poniżej znajdują się wywołania analiz działania modelu: porównanie rekonstrukcji z obrazami wejściowymi, uśrednione punkty reprezentujące każdą klasę, badanie zdolności generatywnych modelu przez modyfikowanie wektora ukrytego oraz wizualizację przestrzeni ukrytej.

In [7]:
analyzer = AutoEncoderAnalyzer(model=ae_model, dataset=val_dataset, n_samplings=1)
In [8]:
analyzer.compare_reconstruction_with_original()
plt.show()
In [9]:
analyzer.average_points_per_class()
plt.show()
In [10]:
for digit, latent_code in enumerate(analyzer._averages):
    print(f"Digit: {digit}")
    analyzer.analyze_features(latent_code, steps=11)
    plt.show()
Digit: 0
Researching values in range [-11.0, 12.0]
Digit: 1
Researching values in range [-11.0, 12.0]
Digit: 2
Researching values in range [-11.0, 12.0]
Digit: 3
Researching values in range [-11.0, 12.0]
Digit: 4
Researching values in range [-11.0, 12.0]
Digit: 5
Researching values in range [-11.0, 12.0]
Digit: 6
Researching values in range [-11.0, 12.0]
Digit: 7
Researching values in range [-11.0, 12.0]
Digit: 8
Researching values in range [-11.0, 12.0]
Digit: 9
Researching values in range [-11.0, 12.0]
In [11]:
analyzer.analyze_tsne()  # this may take quite a long time
plt.show()

Do wytrenowania modelu VAE wykorzystamy bibliotekę Pyro. Funkcja ta dostarcza gotową implementację funkcji kosztu ELBO oraz metody SVI wykorzystywanej do trenowania modelu. Detale implementacyjne znajdują się w przygotowanej funkcji train_ae, którą wykorzystywaliśmy też wcześniej do trenowania modelu autokodera. Pyro wymaga od nas przygotowania funkcji model oraz guide. Pierwszy z nich ma definiować model generatywny $p_\theta(\mathbf{x} | \mathbf{z})p_\theta(\mathbf{z})$, drugi natomiast odpowiada za definicję aproksymacji posteriora $q_\phi(\mathbf{z} | \mathbf{x})$. Z nimi możemy wykorzystać klasę Trace_ELBO, która posłuży jako funkcja kosztu. Zachęcamy do zapoznania się z dokumentacją, gdzie znajduje się więcej informacji na temat modelu oraz guide'a w bibliotece Pyro.

Zadanie 1a (1.5 pkt.)

WzorujÄ…c siÄ™ na implementacji klasycznego autokodera, zaimplementuj model autokodera wariacyjnego:

  1. Przygotuj lub wykorzystaj już przygotowane implementacje kodera oraz dekodera:
    • Zadaniem kodera jest przetworzenie wejÅ›cia (obrazu) do parametrów rozkÅ‚adu (w przypadku rozkÅ‚adu normalnego: Å›redniej i wariancji). Najczęściej parametry te tworzy siÄ™ w ostatniej warstwie, używajÄ…c wspólnych wczeÅ›niejszych warstw. Zwróć uwagÄ™ na zastosowanie odpowiednich funkcji aktywacji: tak, aby nie ograniczać niepotrzebnie zakresu wartoÅ›ci, ale również by nie uzyskać wartoÅ›ci nieprawidÅ‚owych.
    • Zadaniem dekodera jest przetworzenie ukrytej reprezentacji (próbki z rozkÅ‚adu) w celu wygenerowania rekonstrukcji. ZakÅ‚adamy, że próbkowanie odbywa siÄ™ poza dekoderem. Zastosuj odpowiedniÄ… funkcjÄ™ aktywacji na wyjÅ›ciu, tak aby uzyskać wartoÅ›ci o odpowiednim zakresie.
  2. Zaimplementuj klasÄ™ VariationalAutoencoder:
    • Zaimplementuj metodÄ™ encoder_forward, która dla danego wejÅ›cia wygeneruje jego ukrytÄ… reprezentacjÄ™. Wykorzystaj parametry rozkÅ‚adu generowane przez koder oraz wykonaj w tej funkcji próbkowanie.
    • Zaimplementuj metodÄ™ decoder_forward, która dla danej ukrytej reprezentacji (wypróbkowanej z rozkÅ‚adu) wygeneruje jego rekonstrukcjÄ™.

Zwróć uwagę, że w analizach doszło porównanie kilku próbek z rozkładu ukrytego modelu.

Student's task Score: 0.0 / 1.5 (Top)

Zadanie 1b (1.5 pkt.)

Zbadaj wpływ hiperparametrów modelu wariacyjnego autokodera (akie jak liczba neuronów w warstwach ukrytych, rozmiar ukrytej reprezentacji, współczynnik uczenia, itp.) na proces jego trenowania (szybkość, zdolność do wytrenowania, szybkość zbiegania itd.), uzyskiwane rezultaty oraz zdolności generatywne i właściwości przestrzeni ukrytej. Wykorzystaj przygotowaną klasę AutoEncoderAnalyzer. Zapisz wnioski w komórce Markdown.

In [6]:
Student's answer Score: 1.5 / 1.5 (Top)
class VEncoder(nn.Module):
    """Encoder for VAE."""

    def __init__(
        self,
        n_input_features: int,
        n_hidden_neurons: int,
        n_latent_features: int,
    ):
        """
        :param n_input_features: number of input features (28 x 28 = 784 for MNIST)
        :param n_hidden_neurons: number of neurons in hidden FC layer
        :param n_latent_features: size of the latent vector
        """
        super().__init__()

        # TU WPISZ KOD
        self.input_to_hidden = nn.Linear(n_input_features, n_hidden_neurons)
        self.locs = nn.Linear(n_hidden_neurons, n_latent_features)
        self.scales = nn.Linear(n_hidden_neurons, n_latent_features )
        
    def forward(self, x: torch.Tensor) -> Tuple[torch.Tensor, torch.Tensor]:
        """Encode data to gaussian distribution params."""
        z_loc = None
        z_scale = None
        # TU WPISZ KOD
        h = self.input_to_hidden(x)
        h = nn.functional.relu(h)
        z_loc = self.locs(h)
        z_scale = self.scales(h)
        z_scale = nn.functional.softplus(z_scale)
        return z_loc, z_scale
    

class VDecoder(nn.Module):
    """Decoder for VAE."""

    def __init__(
        self,
        n_latent_features: int, 
        n_hidden_neurons: int, 
        n_output_features: int,
    ):
        """
        :param n_latent_features: number of latent features (same as in Encoder)
        :param n_hidden_neurons: number of neurons in hidden FC layer
        :param n_output_features: size of the output vector (28 x 28 = 784 for MNIST)
        """
        super().__init__()

        # TU WPISZ KOD
        self.latent_to_hidden = nn.Linear(n_latent_features, n_hidden_neurons)
        self.hidden_to_output = nn.Linear(n_hidden_neurons, n_output_features)


    def forward(self, z: torch.Tensor) -> torch.Tensor:
        """Decode latent vector to image."""
        r = self.latent_to_hidden(z)
        r = nn.functional.softplus(r)
        r = self.hidden_to_output(r)
        r = torch.sigmoid(r)
        return r


class VariationalAutoencoder(BaseAutoEncoder):
    """Variational Auto Encoder model."""

    def __init__(
        self,
        n_data_features: int,
        n_encoder_hidden_features: int,
        n_decoder_hidden_features: int,
        n_latent_features: int,
    ):
        """
        :param n_data_features: number of input and output features (28 x 28 = 784 for MNIST)
        :param n_encoder_hidden_features: number of neurons in encoder's hidden layer
        :param n_decoder_hidden_features: number of neurons in decoder's hidden layer
        :param n_latent_features: number of latent features
        """
        encoder = VEncoder(
            n_input_features=n_data_features,
            n_hidden_neurons=n_encoder_hidden_features,
            n_latent_features=n_latent_features,
        )
        decoder = VDecoder(
            n_latent_features=n_latent_features,
            n_hidden_neurons=n_decoder_hidden_features,
            n_output_features=n_data_features,
        )
        super().__init__(
            encoder=encoder, decoder=decoder, n_latent_features=n_latent_features
        )
        self.input_shape = None

    def encoder_forward(self, x: torch.Tensor) -> torch.Tensor:
        """Function to perform forward pass through encoder network.
        takes: tensor of shape [batch_size x [image-size]] (input images batch)
        returns: tensor of shape [batch_size x latent_feature_size] (latent vector)
        """
        z = None
        if self.input_shape is None:
            self.input_shape = x.shape[1:]
        x = x.view(x.shape[0], -1)
        # TU WPISZ KOD
        loc, scale = self.encoder(x)
        
        z = loc + scale * torch.randn(scale.shape)
        return z

    def decoder_forward(self, z: torch.Tensor) -> torch.Tensor:
        """Function to perform forward pass through decoder network.
        takes: tensor of shape [batch_size x latent_feature_size] (latent vector)
        returns: tensor of shape [batch_size x [image-size]] (reconstructed images batch)
        """
        r = None
        # TU WPISZ KOD
        r = self.decoder(z)
        return r.view(-1, *self.input_shape)

    def model(self, x: torch.Tensor):
        """Pyro model for VAE; p(x|z)p(z)."""
        pyro.module("decoder", self.decoder)
        with pyro.plate("data", x.shape[0]):
            z_loc = torch.zeros((x.shape[0], self.n_latent_features))
            z_scale = torch.ones((x.shape[0], self.n_latent_features))
            z = pyro.sample("latent", dist.Normal(z_loc, z_scale).to_event(1))
            output = self.decoder.forward(z).view(-1, *self.input_shape)
            pyro.sample("obs", dist.Bernoulli(output).to_event(3), obs=x)

    def guide(self, x: torch.Tensor):
        """Pyro guide for VAE; q(z|x)"""
        pyro.module("encoder", self.encoder)
        with pyro.plate("data", x.shape[0]):
            z_loc, z_scale = self.encoder.forward(x.view(x.shape[0], -1))
            pyro.sample("latent", dist.Normal(z_loc, z_scale).to_event(1))
In [24]:
batch_size = 32
lr = 1e-3
epochs = 20

vae_model = VariationalAutoencoder(
    n_data_features=28 * 28,  # MNIST pixels
    n_encoder_hidden_features=128,  # chosen arbitrarily
    n_decoder_hidden_features=128,  # chosen arbitrarily
    n_latent_features=10,  # how many features will be used to represent input
)

train_dataloader = DataLoader(
    train_dataset,
    batch_size=batch_size,
    shuffle=True,
    drop_last=True,
)
val_loader = DataLoader(
    val_dataset, batch_size=batch_size, shuffle=False, drop_last=False
)

loss_fn = pyro.infer.Trace_ELBO().differentiable_loss
loss_fn_args = (vae_model.model, vae_model.guide)

train_ae(
    vae_model,
    epochs=epochs,
    train_loader=train_dataloader,
    val_loader=val_loader,
    lr=lr,
    loss_fn=loss_fn,
    loss_fn_args=loss_fn_args,
)

Out[24]:
VariationalAutoencoder(
  (encoder): VEncoder(
    (input_to_hidden): Linear(in_features=784, out_features=128, bias=True)
    (locs): Linear(in_features=128, out_features=10, bias=True)
    (scales): Linear(in_features=128, out_features=10, bias=True)
  )
  (decoder): VDecoder(
    (latent_to_hidden): Linear(in_features=10, out_features=128, bias=True)
    (hidden_to_output): Linear(in_features=128, out_features=784, bias=True)
  )
)
In [35]:
analyzer = AutoEncoderAnalyzer(model=vae_model, dataset=val_dataset, n_samplings=5)
In [36]:
analyzer.compare_reconstruction_with_original()
plt.show()
In [37]:
analyzer.compare_samplings()
plt.show()
In [38]:
analyzer.average_points_per_class()
plt.show()
In [39]:
for digit, latent_code in enumerate(analyzer._averages):
    print(f"Digit: {digit}")
    analyzer.analyze_features(latent_code, steps=11)
    plt.show()
Digit: 0
Researching values in range [-5.0, 5.0]
Digit: 1
Researching values in range [-5.0, 5.0]
Digit: 2
Researching values in range [-5.0, 5.0]
Digit: 3
Researching values in range [-5.0, 5.0]
Digit: 4
Researching values in range [-5.0, 5.0]
Digit: 5
Researching values in range [-5.0, 5.0]
Digit: 6
Researching values in range [-5.0, 5.0]
Digit: 7
Researching values in range [-5.0, 5.0]
Digit: 8
Researching values in range [-5.0, 5.0]
Digit: 9
Researching values in range [-5.0, 5.0]
In [40]:
analyzer.analyze_tsne()  # this may take quite a long time
plt.show()

LATENT FEATURES = 30

In [41]:
batch_size = 32
lr = 1e-3
epochs = 20

vae_model = VariationalAutoencoder(
    n_data_features=28 * 28,  # MNIST pixels
    n_encoder_hidden_features=128,  # chosen arbitrarily
    n_decoder_hidden_features=128,  # chosen arbitrarily
    n_latent_features=30,  # how many features will be used to represent input
)

train_dataloader = DataLoader(
    train_dataset,
    batch_size=batch_size,
    shuffle=True,
    drop_last=True,
)
val_loader = DataLoader(
    val_dataset, batch_size=batch_size, shuffle=False, drop_last=False
)

loss_fn = pyro.infer.Trace_ELBO().differentiable_loss
loss_fn_args = (vae_model.model, vae_model.guide)

train_ae(
    vae_model,
    epochs=epochs,
    train_loader=train_dataloader,
    val_loader=val_loader,
    lr=lr,
    loss_fn=loss_fn,
    loss_fn_args=loss_fn_args,
)

Out[41]:
VariationalAutoencoder(
  (encoder): VEncoder(
    (input_to_hidden): Linear(in_features=784, out_features=128, bias=True)
    (locs): Linear(in_features=128, out_features=30, bias=True)
    (scales): Linear(in_features=128, out_features=30, bias=True)
  )
  (decoder): VDecoder(
    (latent_to_hidden): Linear(in_features=30, out_features=128, bias=True)
    (hidden_to_output): Linear(in_features=128, out_features=784, bias=True)
  )
)
In [42]:
analyzer = AutoEncoderAnalyzer(model=vae_model, dataset=val_dataset, n_samplings=5)
analyzer.compare_reconstruction_with_original()
plt.show()

Latent features = 5

In [43]:
batch_size = 32
lr = 1e-3
epochs = 20

vae_model = VariationalAutoencoder(
    n_data_features=28 * 28,  # MNIST pixels
    n_encoder_hidden_features=128,  # chosen arbitrarily
    n_decoder_hidden_features=128,  # chosen arbitrarily
    n_latent_features=5,  # how many features will be used to represent input
)

train_dataloader = DataLoader(
    train_dataset,
    batch_size=batch_size,
    shuffle=True,
    drop_last=True,
)
val_loader = DataLoader(
    val_dataset, batch_size=batch_size, shuffle=False, drop_last=False
)

loss_fn = pyro.infer.Trace_ELBO().differentiable_loss
loss_fn_args = (vae_model.model, vae_model.guide)

train_ae(
    vae_model,
    epochs=epochs,
    train_loader=train_dataloader,
    val_loader=val_loader,
    lr=lr,
    loss_fn=loss_fn,
    loss_fn_args=loss_fn_args,
)

analyzer = AutoEncoderAnalyzer(model=vae_model, dataset=val_dataset, n_samplings=5)
analyzer.compare_reconstruction_with_original()
plt.show()

Number of hidden features = 40

In [44]:
batch_size = 32
lr = 1e-3
epochs = 20

vae_model = VariationalAutoencoder(
    n_data_features=28 * 28,  # MNIST pixels
    n_encoder_hidden_features=40,  # chosen arbitrarily
    n_decoder_hidden_features=40,  # chosen arbitrarily
    n_latent_features=10,  # how many features will be used to represent input
)

train_dataloader = DataLoader(
    train_dataset,
    batch_size=batch_size,
    shuffle=True,
    drop_last=True,
)
val_loader = DataLoader(
    val_dataset, batch_size=batch_size, shuffle=False, drop_last=False
)

loss_fn = pyro.infer.Trace_ELBO().differentiable_loss
loss_fn_args = (vae_model.model, vae_model.guide)

train_ae(
    vae_model,
    epochs=epochs,
    train_loader=train_dataloader,
    val_loader=val_loader,
    lr=lr,
    loss_fn=loss_fn,
    loss_fn_args=loss_fn_args,
)

analyzer = AutoEncoderAnalyzer(model=vae_model, dataset=val_dataset, n_samplings=5)
analyzer.compare_reconstruction_with_original()
plt.show()

Zastosowanie wymiaru przestrzeni ukrytej = 30

Number of features = 256

In [45]:
batch_size = 32
lr = 1e-3
epochs = 20

vae_model = VariationalAutoencoder(
    n_data_features=28 * 28,  # MNIST pixels
    n_encoder_hidden_features=256,  # chosen arbitrarily
    n_decoder_hidden_features=256,  # chosen arbitrarily
    n_latent_features=10,  # how many features will be used to represent input
)

train_dataloader = DataLoader(
    train_dataset,
    batch_size=batch_size,
    shuffle=True,
    drop_last=True,
)
val_loader = DataLoader(
    val_dataset, batch_size=batch_size, shuffle=False, drop_last=False
)

loss_fn = pyro.infer.Trace_ELBO().differentiable_loss
loss_fn_args = (vae_model.model, vae_model.guide)

train_ae(
    vae_model,
    epochs=epochs,
    train_loader=train_dataloader,
    val_loader=val_loader,
    lr=lr,
    loss_fn=loss_fn,
    loss_fn_args=loss_fn_args,
)

analyzer = AutoEncoderAnalyzer(model=vae_model, dataset=val_dataset, n_samplings=5)
analyzer.compare_reconstruction_with_original()
plt.show()

Współczynnik uczenia = 0.01

In [46]:
batch_size = 32
lr = 1e-2
epochs = 20

vae_model = VariationalAutoencoder(
    n_data_features=28 * 28,  # MNIST pixels
    n_encoder_hidden_features=128,  # chosen arbitrarily
    n_decoder_hidden_features=128,  # chosen arbitrarily
    n_latent_features=10,  # how many features will be used to represent input
)

train_dataloader = DataLoader(
    train_dataset,
    batch_size=batch_size,
    shuffle=True,
    drop_last=True,
)
val_loader = DataLoader(
    val_dataset, batch_size=batch_size, shuffle=False, drop_last=False
)

loss_fn = pyro.infer.Trace_ELBO().differentiable_loss
loss_fn_args = (vae_model.model, vae_model.guide)

train_ae(
    vae_model,
    epochs=epochs,
    train_loader=train_dataloader,
    val_loader=val_loader,
    lr=lr,
    loss_fn=loss_fn,
    loss_fn_args=loss_fn_args,
)

analyzer = AutoEncoderAnalyzer(model=vae_model, dataset=val_dataset, n_samplings=5)
analyzer.compare_reconstruction_with_original()
plt.show()

Współczynnik uczenia = 0.0001

In [47]:
batch_size = 32
lr = 1e-4
epochs = 20

vae_model = VariationalAutoencoder(
    n_data_features=28 * 28,  # MNIST pixels
    n_encoder_hidden_features=128,  # chosen arbitrarily
    n_decoder_hidden_features=128,  # chosen arbitrarily
    n_latent_features=10,  # how many features will be used to represent input
)

train_dataloader = DataLoader(
    train_dataset,
    batch_size=batch_size,
    shuffle=True,
    drop_last=True,
)
val_loader = DataLoader(
    val_dataset, batch_size=batch_size, shuffle=False, drop_last=False
)

loss_fn = pyro.infer.Trace_ELBO().differentiable_loss
loss_fn_args = (vae_model.model, vae_model.guide)

train_ae(
    vae_model,
    epochs=epochs,
    train_loader=train_dataloader,
    val_loader=val_loader,
    lr=lr,
    loss_fn=loss_fn,
    loss_fn_args=loss_fn_args,
)

analyzer = AutoEncoderAnalyzer(model=vae_model, dataset=val_dataset, n_samplings=5)
analyzer.compare_reconstruction_with_original()
plt.show()

Batch size = 2

In [48]:
batch_size = 2
lr = 1e-3
epochs = 20

vae_model = VariationalAutoencoder(
    n_data_features=28 * 28,  # MNIST pixels
    n_encoder_hidden_features=128,  # chosen arbitrarily
    n_decoder_hidden_features=128,  # chosen arbitrarily
    n_latent_features=10,  # how many features will be used to represent input
)

train_dataloader = DataLoader(
    train_dataset,
    batch_size=batch_size,
    shuffle=True,
    drop_last=True,
)
val_loader = DataLoader(
    val_dataset, batch_size=batch_size, shuffle=False, drop_last=False
)

loss_fn = pyro.infer.Trace_ELBO().differentiable_loss
loss_fn_args = (vae_model.model, vae_model.guide)

train_ae(
    vae_model,
    epochs=epochs,
    train_loader=train_dataloader,
    val_loader=val_loader,
    lr=lr,
    loss_fn=loss_fn,
    loss_fn_args=loss_fn_args,
)

analyzer = AutoEncoderAnalyzer(model=vae_model, dataset=val_dataset, n_samplings=5)
analyzer.compare_reconstruction_with_original()
plt.show()

Zastosowanie wymiaru warstwy ukrytej = 30 nie przyniosło znacznej poprawy względem wymiaru = 10. Natomiast zastosowanie wymiaru = 5 sprawiło, że generowane obrazy były 'rozmyte'. Model nauczył się nie więcej kształtu danej cyfry, bez możliwości rysowania ostrych granic. Wynika to z niedostatecznej zdolności do zakodowania pełnej informacji w 5 wymiarowej przestrzenii ukrytej. Wykorzystanie 40 neuronów w warstwach ukrytych zamiast 128 sprawiło, że model uczył się troche wolniej. Jednakże ostatecznie jego zdolności generatywne są zbliżone do modelu zawierającego 128 neuronów ukrytych (a przynajmniej wykorzystując ludzkie oko jako przyrzad pomiarowy). Zastosowanie natomiast 256 neuronów, nie przyniosło znacznej poprawy względem domyślnej konfiguracji, co może być związane z 'przystępnym poziomem trudności' zbioru MNIST (tak duża liczba neuronów nie jest potrzebna do rozwiązywania tego zadania). Wpływ współczynnika uczenia był spodziewany. Większe wartości pozwalają na szybsze dojście do poziomu wysycenia krzywej uczenia, a mniejsze sprawiają, że model dłużej dąży do tego optimum. Na koniec zbadano wpływ zastosowania bardzo małego batch'a o rozmiarze 2. Wyniki pokazują jak chaotyczny jest przebieg uczenia. Jednakże obserwując wygenerowane próbki można uznać, że model wyuczył się zdolności generatywnych.

Zadanie 2 (2 pkt.)

Metody uczenia reprezentacji, do których można zaliczyć autokoder wariacyjny, są często sprawdzane pod względem możliwości ich zastosowania w tzw. downstream tasks, czyli prostych zadaniach mających na celu weryfikację jakości utworzonej reprezentacji danych. Polegają one np. na wytrenowaniu modelu do jakiegoś zadania nie na danych, ale na ich reprezentacji, wytworzonej przez model uczenia reprezentacji. W tym przypadku tym zadaniem będzie klasyfikacja cyfr.

Wybierz dowolny klasyfikator (ważne: klasyfikator ten powinien osiągać słabe rezultaty dla zbioru MNIST). Zbadaj, jakie wartości metryk osiąga on przy zastosowaniu wprost na danych ze zbioru MNIST; sprawdź także ile czasu zajmuje trenowanie klasyfikatora oraz wnioskowanie.

Następnie zastosuj ten sam klasyfikator na ukrytych reprezentacjach wytworzonych przez oba modele autokodera: Autoencoder oraz VariationalAutoencoder, wytrenowane wcześniej (odpowiednio ae_model oraz vae_model). Przetwórz cały zbiór treningowy i walidacyjny z użyciem kodera w celu uzyskania ukrytych reprezentacji przykładów, a następnie wykorzystaj je do wytrenowania prostego klasyfikatora. Porównaj uzyskane metryki oraz szybkość działania.

In [49]:
Student's answer Score: 2.0 / 2.0 (Top)
X_train, y_train = zip(*train_dataset)
X_train = torch.stack(X_train)
X_val, y_val = zip(*val_dataset)
X_val = torch.stack(X_val)


# TU WPISZ KOD

X_train.shape
Out[49]:
torch.Size([10000, 1, 28, 28])
In [59]:
from sklearn.neighbors import KNeighborsClassifier
from sklearn.metrics import classification_report
from sklearn.svm import SVC
from sklearn.discriminant_analysis import QuadraticDiscriminantAnalysis
In [57]:
neigh = KNeighborsClassifier(n_neighbors=10)

neigh.fit(X_train.view(X_train.shape[0],-1), y_train)

prediction = neigh.predict(X_val.view(X_val.shape[0], -1))
print(classification_report(y_val, prediction))
              precision    recall  f1-score   support

           0       0.94      0.99      0.96       980
           1       0.89      1.00      0.94      1135
           2       0.98      0.89      0.94      1032
           3       0.94      0.95      0.95      1010
           4       0.97      0.92      0.94       982
           5       0.95      0.93      0.94       892
           6       0.96      0.98      0.97       958
           7       0.93      0.93      0.93      1028
           8       0.98      0.89      0.93       974
           9       0.91      0.94      0.93      1009

    accuracy                           0.94     10000
   macro avg       0.94      0.94      0.94     10000
weighted avg       0.94      0.94      0.94     10000

In [58]:
svc = SVC()
svc.fit(X_train.view(X_train.shape[0], -1), y_train)

prediction = svc.predict(X_val.view(X_val.shape[0], -1))
print(classification_report(y_val, prediction))
              precision    recall  f1-score   support

           0       0.97      0.99      0.98       980
           1       0.98      0.99      0.99      1135
           2       0.96      0.96      0.96      1032
           3       0.95      0.97      0.96      1010
           4       0.96      0.96      0.96       982
           5       0.96      0.96      0.96       892
           6       0.97      0.97      0.97       958
           7       0.97      0.94      0.96      1028
           8       0.96      0.94      0.95       974
           9       0.95      0.94      0.95      1009

    accuracy                           0.96     10000
   macro avg       0.96      0.96      0.96     10000
weighted avg       0.96      0.96      0.96     10000

In [12]:
class WeakClassifier(nn.Module):
    def __init__(self, input_size, hidden_size, output_size):
        super().__init__()
        self.hidden_size = hidden_size
        self.input_size = input_size
        self.output_size = output_size
        
        self.model = nn.Sequential(
            nn.Linear(self.input_size, self.hidden_size),
            nn.ReLU(inplace=True),
            nn.Linear(self.hidden_size, self.output_size)
        )
        
    def forward(self, x):
        x = x.view(x.shape[0], -1)
        return self.model(x)
    
    
    
In [10]:
import torch.optim as optim
from torch.utils.data import DataLoader, Dataset
from tqdm.auto import tqdm, trange


def count_correct(
    y_pred: torch.Tensor, y_true: torch.Tensor
) -> torch.Tensor:
    preds = torch.argmax(y_pred, dim=1)
    return (preds == y_true).float().sum()

def validate(
    model: nn.Module, 
    loss_fn: torch.nn.CrossEntropyLoss, 
    dataloader: DataLoader
) -> Tuple[torch.Tensor, torch.Tensor]:
    loss = 0
    correct = 0
    all = 0
    for X_batch, y_batch in dataloader:
        y_pred = model(X_batch)
        all += len(y_pred)
        loss += loss_fn(y_pred, y_batch).sum()
        correct += count_correct(y_pred, y_batch)
    return loss / all, correct / all


def fit(model: nn.Module, optimizer: optim.Optimizer,
       loss_fn: nn.CrossEntropyLoss, train_dl: DataLoader, 
        val_dl: DataLoader, epochs: int, print_metrics: str = True):
    
    train_metrics = {
        'loss': [],
        'acc': []
    }
    
    val_metrics = {
        'loss': [],
        'acc': []
    }
    
    for epoch in trange(epochs, desc='epoch'):
        model.train()
        pbar = tqdm(train_dl, desc='step', leave=False)
        for X_batch, y_batch in pbar:
            y_pred = model(X_batch)
            loss = loss_fn(y_pred, y_batch)
            
            loss.backward()
            optimizer.step()
            optimizer.zero_grad()
            
            pbar.update(1)
        pbar.close()
        
        if print_metrics:
            model.eval()
            with torch.no_grad():
                train_loss, train_acc = validate(
                    model=model, loss_fn=loss_fn, dataloader=train_dl
                )
                val_loss, val_acc = validate(
                    model=model, loss_fn=loss_fn, dataloader=val_dl
                )
                
                print(
                    f"Epoch {epoch}:"
                    f"train loss = {train_loss:.3f} (acc: {train_acc:.3f}), "
                    f"validation loss = {val_loss:.3f} (acc: {val_acc:.3f})"
                )
            train_metrics['loss'].append(train_loss)
            train_metrics['acc'].append(train_acc)
            val_metrics['loss'].append(val_loss)
            val_metrics['acc'].append(val_acc)
            
    if print_metrics:  
        plot_results(train_metrics, val_metrics)
    return model
    
def plot_results(train_metrics, val_metrics):
    fig, (ax1, ax2) = plt.subplots(1,2, figsize=(12,6))
    
    ax1.plot(train_metrics['loss'], label='train')
    ax1.plot(val_metrics['loss'], label='val')
    ax1.set_title('Loss (Epoch)')
    ax1.set_xlabel('Epoch')
    ax1.set_ylabel('Loss value')
    ax2.plot(train_metrics['acc'], label='train')
    ax2.plot(val_metrics['acc'], label='val')
    ax2.set_title('ACC (Epoch)')
    ax2.set_xlabel('Epoch')
    ax2.set_ylabel('ACC value')
    
    ax1.legend()
    ax2.legend()
    
    
            
    
In [26]:
train_dataloader = DataLoader(
    train_dataset,
    batch_size=batch_size,
    shuffle=True,
    drop_last=True,
)
val_loader = DataLoader(
    val_dataset, batch_size=batch_size, shuffle=False, drop_last=False
)

weak_classifier = WeakClassifier(input_size=28*28, hidden_size=5,output_size=10)
optimiser = optim.Adam(weak_classifier.parameters())
loss_fn = nn.CrossEntropyLoss()

weak_classifier = fit(weak_classifier, optimiser, loss_fn, train_dataloader, val_loader, 
                     50, True)
Epoch 0:train loss = 0.033 (acc: 0.710), validation loss = 0.032 (acc: 0.718)
Epoch 1:train loss = 0.023 (acc: 0.781), validation loss = 0.023 (acc: 0.789)
Epoch 2:train loss = 0.020 (acc: 0.811), validation loss = 0.020 (acc: 0.817)
Epoch 3:train loss = 0.018 (acc: 0.826), validation loss = 0.019 (acc: 0.832)
Epoch 4:train loss = 0.017 (acc: 0.842), validation loss = 0.017 (acc: 0.844)
Epoch 5:train loss = 0.016 (acc: 0.851), validation loss = 0.017 (acc: 0.850)
Epoch 6:train loss = 0.016 (acc: 0.856), validation loss = 0.016 (acc: 0.857)
Epoch 7:train loss = 0.015 (acc: 0.861), validation loss = 0.016 (acc: 0.858)
Epoch 8:train loss = 0.015 (acc: 0.866), validation loss = 0.015 (acc: 0.863)
Epoch 9:train loss = 0.014 (acc: 0.871), validation loss = 0.015 (acc: 0.865)
Epoch 10:train loss = 0.014 (acc: 0.876), validation loss = 0.015 (acc: 0.869)
Epoch 11:train loss = 0.013 (acc: 0.878), validation loss = 0.015 (acc: 0.871)
Epoch 12:train loss = 0.013 (acc: 0.879), validation loss = 0.015 (acc: 0.872)
Epoch 13:train loss = 0.013 (acc: 0.883), validation loss = 0.014 (acc: 0.874)
Epoch 14:train loss = 0.012 (acc: 0.886), validation loss = 0.014 (acc: 0.878)
Epoch 15:train loss = 0.012 (acc: 0.889), validation loss = 0.014 (acc: 0.880)
Epoch 16:train loss = 0.012 (acc: 0.891), validation loss = 0.014 (acc: 0.882)
Epoch 17:train loss = 0.012 (acc: 0.892), validation loss = 0.014 (acc: 0.881)
Epoch 18:train loss = 0.011 (acc: 0.894), validation loss = 0.013 (acc: 0.885)
Epoch 19:train loss = 0.011 (acc: 0.895), validation loss = 0.013 (acc: 0.884)
Epoch 20:train loss = 0.011 (acc: 0.896), validation loss = 0.013 (acc: 0.883)
Epoch 21:train loss = 0.011 (acc: 0.902), validation loss = 0.013 (acc: 0.886)
Epoch 22:train loss = 0.011 (acc: 0.902), validation loss = 0.013 (acc: 0.887)
Epoch 23:train loss = 0.010 (acc: 0.902), validation loss = 0.013 (acc: 0.888)
Epoch 24:train loss = 0.011 (acc: 0.899), validation loss = 0.013 (acc: 0.883)
Epoch 25:train loss = 0.010 (acc: 0.902), validation loss = 0.013 (acc: 0.887)
Epoch 26:train loss = 0.010 (acc: 0.905), validation loss = 0.013 (acc: 0.887)
Epoch 27:train loss = 0.010 (acc: 0.906), validation loss = 0.013 (acc: 0.891)
Epoch 28:train loss = 0.010 (acc: 0.904), validation loss = 0.013 (acc: 0.890)
Epoch 29:train loss = 0.010 (acc: 0.908), validation loss = 0.013 (acc: 0.889)
Epoch 30:train loss = 0.010 (acc: 0.904), validation loss = 0.013 (acc: 0.887)
Epoch 31:train loss = 0.010 (acc: 0.908), validation loss = 0.013 (acc: 0.890)
Epoch 32:train loss = 0.010 (acc: 0.909), validation loss = 0.013 (acc: 0.891)
Epoch 33:train loss = 0.009 (acc: 0.909), validation loss = 0.013 (acc: 0.891)
Epoch 34:train loss = 0.009 (acc: 0.912), validation loss = 0.013 (acc: 0.892)
Epoch 35:train loss = 0.009 (acc: 0.910), validation loss = 0.013 (acc: 0.890)
Epoch 36:train loss = 0.009 (acc: 0.913), validation loss = 0.013 (acc: 0.894)
Epoch 37:train loss = 0.009 (acc: 0.911), validation loss = 0.013 (acc: 0.890)
Epoch 38:train loss = 0.009 (acc: 0.911), validation loss = 0.013 (acc: 0.888)
Epoch 39:train loss = 0.009 (acc: 0.912), validation loss = 0.013 (acc: 0.889)
Epoch 40:train loss = 0.009 (acc: 0.915), validation loss = 0.013 (acc: 0.893)
Epoch 41:train loss = 0.009 (acc: 0.914), validation loss = 0.013 (acc: 0.890)
Epoch 42:train loss = 0.009 (acc: 0.916), validation loss = 0.013 (acc: 0.889)
Epoch 43:train loss = 0.009 (acc: 0.917), validation loss = 0.013 (acc: 0.893)
Epoch 44:train loss = 0.009 (acc: 0.918), validation loss = 0.013 (acc: 0.893)
Epoch 45:train loss = 0.009 (acc: 0.915), validation loss = 0.013 (acc: 0.892)
Epoch 46:train loss = 0.009 (acc: 0.914), validation loss = 0.013 (acc: 0.891)
Epoch 47:train loss = 0.009 (acc: 0.918), validation loss = 0.013 (acc: 0.891)
Epoch 48:train loss = 0.009 (acc: 0.918), validation loss = 0.013 (acc: 0.891)
Epoch 49:train loss = 0.009 (acc: 0.920), validation loss = 0.013 (acc: 0.892)

In [22]:
class WeakClassifierWithAutoEncoder(nn.Module):
    def __init__(self, latent_size, hidden_size, output_size, encoder):
        super().__init__()
        self.hidden_size = hidden_size
        self.latent_size = latent_size
        self.output_size = output_size
        self.encoder = encoder
        for param in self.encoder.parameters():
            param.requires_grad = False
        
        self.model = nn.Sequential(
            nn.Linear(self.latent_size, self.hidden_size),
            nn.ReLU(inplace=True),
            nn.Linear(self.hidden_size, self.output_size)
        )
        
    def forward(self, x):
        x = x.view(x.shape[0], -1)
        
        latent = self.encoder.encoder_forward(x)
        
        return self.model(latent)
    
In [27]:
train_dataloader = DataLoader(
    train_dataset,
    batch_size=batch_size,
    shuffle=True,
    drop_last=True,
)
val_loader = DataLoader(
    val_dataset, batch_size=batch_size, shuffle=False, drop_last=False
)

weak_classifier = WeakClassifierWithAutoEncoder(latent_size=10, hidden_size=5, 
                                                output_size=10, encoder=ae_model)
optimiser = optim.Adam(weak_classifier.parameters())
loss_fn = nn.CrossEntropyLoss()

weak_classifier = fit(weak_classifier, optimiser, loss_fn, train_dataloader, val_loader, 
                     50, True)
Epoch 0:train loss = 0.048 (acc: 0.478), validation loss = 0.048 (acc: 0.480)
Epoch 1:train loss = 0.035 (acc: 0.628), validation loss = 0.035 (acc: 0.636)
Epoch 2:train loss = 0.030 (acc: 0.698), validation loss = 0.030 (acc: 0.708)
Epoch 3:train loss = 0.027 (acc: 0.735), validation loss = 0.027 (acc: 0.746)
Epoch 4:train loss = 0.025 (acc: 0.751), validation loss = 0.025 (acc: 0.760)
Epoch 5:train loss = 0.024 (acc: 0.763), validation loss = 0.024 (acc: 0.773)
Epoch 6:train loss = 0.023 (acc: 0.770), validation loss = 0.023 (acc: 0.779)
Epoch 7:train loss = 0.023 (acc: 0.775), validation loss = 0.022 (acc: 0.781)
Epoch 8:train loss = 0.022 (acc: 0.778), validation loss = 0.022 (acc: 0.785)
Epoch 9:train loss = 0.022 (acc: 0.784), validation loss = 0.021 (acc: 0.789)
Epoch 10:train loss = 0.021 (acc: 0.787), validation loss = 0.021 (acc: 0.792)
Epoch 11:train loss = 0.021 (acc: 0.788), validation loss = 0.021 (acc: 0.794)
Epoch 12:train loss = 0.021 (acc: 0.788), validation loss = 0.020 (acc: 0.796)
Epoch 13:train loss = 0.020 (acc: 0.789), validation loss = 0.020 (acc: 0.798)
Epoch 14:train loss = 0.020 (acc: 0.791), validation loss = 0.020 (acc: 0.799)
Epoch 15:train loss = 0.020 (acc: 0.794), validation loss = 0.020 (acc: 0.803)
Epoch 16:train loss = 0.020 (acc: 0.794), validation loss = 0.019 (acc: 0.801)
Epoch 17:train loss = 0.020 (acc: 0.797), validation loss = 0.019 (acc: 0.804)
Epoch 18:train loss = 0.019 (acc: 0.797), validation loss = 0.019 (acc: 0.803)
Epoch 19:train loss = 0.019 (acc: 0.799), validation loss = 0.019 (acc: 0.805)
Epoch 20:train loss = 0.019 (acc: 0.799), validation loss = 0.019 (acc: 0.807)
Epoch 21:train loss = 0.019 (acc: 0.803), validation loss = 0.019 (acc: 0.807)
Epoch 22:train loss = 0.019 (acc: 0.802), validation loss = 0.019 (acc: 0.807)
Epoch 23:train loss = 0.019 (acc: 0.802), validation loss = 0.019 (acc: 0.807)
Epoch 24:train loss = 0.019 (acc: 0.804), validation loss = 0.019 (acc: 0.809)
Epoch 25:train loss = 0.019 (acc: 0.802), validation loss = 0.019 (acc: 0.808)
Epoch 26:train loss = 0.019 (acc: 0.806), validation loss = 0.019 (acc: 0.810)
Epoch 27:train loss = 0.019 (acc: 0.805), validation loss = 0.019 (acc: 0.808)
Epoch 28:train loss = 0.018 (acc: 0.805), validation loss = 0.019 (acc: 0.809)
Epoch 29:train loss = 0.018 (acc: 0.808), validation loss = 0.018 (acc: 0.811)
Epoch 30:train loss = 0.018 (acc: 0.808), validation loss = 0.018 (acc: 0.811)
Epoch 31:train loss = 0.018 (acc: 0.809), validation loss = 0.018 (acc: 0.812)
Epoch 32:train loss = 0.018 (acc: 0.810), validation loss = 0.018 (acc: 0.813)
Epoch 33:train loss = 0.018 (acc: 0.810), validation loss = 0.018 (acc: 0.814)
Epoch 34:train loss = 0.018 (acc: 0.807), validation loss = 0.018 (acc: 0.813)
Epoch 35:train loss = 0.018 (acc: 0.810), validation loss = 0.018 (acc: 0.815)
Epoch 36:train loss = 0.018 (acc: 0.812), validation loss = 0.018 (acc: 0.814)
Epoch 37:train loss = 0.018 (acc: 0.813), validation loss = 0.018 (acc: 0.816)
Epoch 38:train loss = 0.018 (acc: 0.812), validation loss = 0.018 (acc: 0.815)
Epoch 39:train loss = 0.018 (acc: 0.812), validation loss = 0.018 (acc: 0.816)
Epoch 40:train loss = 0.018 (acc: 0.814), validation loss = 0.018 (acc: 0.818)
Epoch 41:train loss = 0.018 (acc: 0.812), validation loss = 0.018 (acc: 0.816)
Epoch 42:train loss = 0.018 (acc: 0.815), validation loss = 0.018 (acc: 0.817)
Epoch 43:train loss = 0.018 (acc: 0.816), validation loss = 0.018 (acc: 0.819)
Epoch 44:train loss = 0.018 (acc: 0.817), validation loss = 0.018 (acc: 0.819)
Epoch 45:train loss = 0.018 (acc: 0.815), validation loss = 0.018 (acc: 0.817)
Epoch 46:train loss = 0.018 (acc: 0.818), validation loss = 0.018 (acc: 0.818)
Epoch 47:train loss = 0.018 (acc: 0.819), validation loss = 0.018 (acc: 0.818)
Epoch 48:train loss = 0.018 (acc: 0.818), validation loss = 0.018 (acc: 0.820)
Epoch 49:train loss = 0.018 (acc: 0.818), validation loss = 0.018 (acc: 0.820)

In [28]:
train_dataloader = DataLoader(
    train_dataset,
    batch_size=batch_size,
    shuffle=True,
    drop_last=True,
)
val_loader = DataLoader(
    val_dataset, batch_size=batch_size, shuffle=False, drop_last=False
)

weak_classifier = WeakClassifierWithAutoEncoder(latent_size=10, hidden_size=5, 
                                                output_size=10, encoder=vae_model)
optimiser = optim.Adam(weak_classifier.parameters())
loss_fn = nn.CrossEntropyLoss()

weak_classifier = fit(weak_classifier, optimiser, loss_fn, train_dataloader, val_loader, 
                     50, True)
Epoch 0:train loss = 0.057 (acc: 0.432), validation loss = 0.057 (acc: 0.434)
Epoch 1:train loss = 0.042 (acc: 0.572), validation loss = 0.042 (acc: 0.577)
Epoch 2:train loss = 0.033 (acc: 0.650), validation loss = 0.033 (acc: 0.663)
Epoch 3:train loss = 0.030 (acc: 0.689), validation loss = 0.029 (acc: 0.701)
Epoch 4:train loss = 0.027 (acc: 0.711), validation loss = 0.027 (acc: 0.721)
Epoch 5:train loss = 0.026 (acc: 0.718), validation loss = 0.025 (acc: 0.739)
Epoch 6:train loss = 0.025 (acc: 0.727), validation loss = 0.025 (acc: 0.739)
Epoch 7:train loss = 0.025 (acc: 0.735), validation loss = 0.024 (acc: 0.751)
Epoch 8:train loss = 0.024 (acc: 0.741), validation loss = 0.023 (acc: 0.760)
Epoch 9:train loss = 0.024 (acc: 0.742), validation loss = 0.023 (acc: 0.758)
Epoch 10:train loss = 0.023 (acc: 0.752), validation loss = 0.023 (acc: 0.763)
Epoch 11:train loss = 0.023 (acc: 0.751), validation loss = 0.022 (acc: 0.768)
Epoch 12:train loss = 0.023 (acc: 0.755), validation loss = 0.022 (acc: 0.769)
Epoch 13:train loss = 0.022 (acc: 0.759), validation loss = 0.022 (acc: 0.774)
Epoch 14:train loss = 0.022 (acc: 0.760), validation loss = 0.022 (acc: 0.773)
Epoch 15:train loss = 0.022 (acc: 0.765), validation loss = 0.021 (acc: 0.777)
Epoch 16:train loss = 0.022 (acc: 0.768), validation loss = 0.022 (acc: 0.771)
Epoch 17:train loss = 0.022 (acc: 0.764), validation loss = 0.021 (acc: 0.776)
Epoch 18:train loss = 0.022 (acc: 0.767), validation loss = 0.021 (acc: 0.779)
Epoch 19:train loss = 0.022 (acc: 0.766), validation loss = 0.021 (acc: 0.782)
Epoch 20:train loss = 0.022 (acc: 0.769), validation loss = 0.021 (acc: 0.778)
Epoch 21:train loss = 0.022 (acc: 0.770), validation loss = 0.021 (acc: 0.780)
Epoch 22:train loss = 0.021 (acc: 0.773), validation loss = 0.021 (acc: 0.782)
Epoch 23:train loss = 0.021 (acc: 0.767), validation loss = 0.021 (acc: 0.779)
Epoch 24:train loss = 0.021 (acc: 0.770), validation loss = 0.021 (acc: 0.780)
Epoch 25:train loss = 0.021 (acc: 0.772), validation loss = 0.021 (acc: 0.780)
Epoch 26:train loss = 0.021 (acc: 0.779), validation loss = 0.021 (acc: 0.782)
Epoch 27:train loss = 0.021 (acc: 0.772), validation loss = 0.021 (acc: 0.780)
Epoch 28:train loss = 0.021 (acc: 0.770), validation loss = 0.021 (acc: 0.780)
Epoch 29:train loss = 0.021 (acc: 0.771), validation loss = 0.021 (acc: 0.785)
Epoch 30:train loss = 0.021 (acc: 0.772), validation loss = 0.021 (acc: 0.782)
Epoch 31:train loss = 0.021 (acc: 0.771), validation loss = 0.021 (acc: 0.782)
Epoch 32:train loss = 0.021 (acc: 0.775), validation loss = 0.021 (acc: 0.780)
Epoch 33:train loss = 0.021 (acc: 0.776), validation loss = 0.021 (acc: 0.778)
Epoch 34:train loss = 0.021 (acc: 0.771), validation loss = 0.021 (acc: 0.778)
Epoch 35:train loss = 0.021 (acc: 0.776), validation loss = 0.020 (acc: 0.786)
Epoch 36:train loss = 0.021 (acc: 0.777), validation loss = 0.021 (acc: 0.781)
Epoch 37:train loss = 0.021 (acc: 0.778), validation loss = 0.020 (acc: 0.782)
Epoch 38:train loss = 0.021 (acc: 0.776), validation loss = 0.021 (acc: 0.784)
Epoch 39:train loss = 0.021 (acc: 0.772), validation loss = 0.021 (acc: 0.782)
Epoch 40:train loss = 0.021 (acc: 0.775), validation loss = 0.021 (acc: 0.782)
Epoch 41:train loss = 0.021 (acc: 0.774), validation loss = 0.021 (acc: 0.779)
Epoch 42:train loss = 0.021 (acc: 0.773), validation loss = 0.020 (acc: 0.784)
Epoch 43:train loss = 0.021 (acc: 0.775), validation loss = 0.020 (acc: 0.785)
Epoch 44:train loss = 0.021 (acc: 0.773), validation loss = 0.020 (acc: 0.783)
Epoch 45:train loss = 0.021 (acc: 0.773), validation loss = 0.021 (acc: 0.783)
Epoch 46:train loss = 0.021 (acc: 0.773), validation loss = 0.021 (acc: 0.782)
Epoch 47:train loss = 0.021 (acc: 0.768), validation loss = 0.020 (acc: 0.787)
Epoch 48:train loss = 0.021 (acc: 0.774), validation loss = 0.020 (acc: 0.782)
Epoch 49:train loss = 0.021 (acc: 0.773), validation loss = 0.020 (acc: 0.781)

Wykorzystując latent space z AE oraz VAE osiągano trochę gorsze rezultaty niż w przypadku zwykłej sieci neuronowej trenowanej end-to-end. Jednakże w przypadku modeli korzystających z latent space'a zauważono znacznie mniejszy potencjał do przetrenowania, co w dłuższej perspektywie mogłoby wpłynąć na przewagę tych 2 modeli (lepsze zdolności generalizacyjne).

Zadanie 3 (dodatkowe) (2 pkt.)

Jednym z fundamentalnych celów uczenia reprezentacji jest dążenie do uzyskania rozłącznych cech (co oznacza, że zmiana pojedynczego elementu wektora ukrytego spowoduje zmianę tylko jednej cechy obrazu wyjściowego). Poprzednie modele nie są w stanie uzyskać tego rezultatu - zmiana pojedynczego elementu wektora wpływa zazwyczaj na więcej niż jedną cechę obrazu wyjściowego.

Jedno z rozwiązań pozwalające na uzyskanie rozłącznych cech jest $\beta$-VAE. Zaproponowana modyfikacja polega na wprowadzeniu współczynnika regularyzacji $\beta$ do funkcji kosztu, dzięki któremu możemy regulować wpływ regularyzacji aproksymacji posteriora na rezultaty trenowania:

$$\log p_\theta \left(\mathbf{x}\right) \ge \mathcal{L}\left(\mathbf{x}, \theta, \phi, \beta\right) = \underbrace{\mathbb{E}_{\mathbf{z}}\left[\log p_\theta\left(\mathbf{x} | \mathbf{z}\right)\right]}_{\text{błąd rekonstrukcji}} - \overbrace{\beta}^{\text{współczynnik regularyzacji}}\underbrace{\left(D_{KL}\left(q_\phi\left(\mathbf{z} | \mathbf{x}\right)\| p\left(\mathbf{z}\right)\right)\right)}_{\text{regularyzacja aproksymacji posteriora}}.$$

Publikacja: link

Zadanie polega na implementacji modelu $\beta$-VAE. Wykorzystaj jak najwięcej komponentów klasy VariationalAutoencoder. Podpowiedź: należy zmodyfikować model oraz guide, wykorzystując narzędzia modyfikujące obliczanie score'ów (effect handlers) w Pyro: Poutine. Przeanalizuj model z użyciem AutoEncoderAnalyzer - w szczególności pod względem uzyskiwanej reprezentacji ukrytej, zdolności generatywnych oraz wpływu zmian współczynnika $\beta$.

In [7]:
Student's answer Score: 0.0 / 0.0 (Top)
class BetaVariationalAutoencoder(VariationalAutoencoder):
    """beta-Variational Auto Encoder model."""
    def __init__(
        self,
        n_data_features: int,
        n_encoder_hidden_features: int,
        n_decoder_hidden_features: int,
        n_latent_features: int,
        beta: float):
        
        super().__init__(n_data_features, n_encoder_hidden_features, 
                        n_decoder_hidden_features, n_latent_features)
        
        self.beta = beta
        
    
    # TU WPISZ KOD
    def model(self, x: torch.Tensor):
        """Pyro model for VAE; p(x|z)p(z)."""
        pyro.module("decoder", self.decoder)
        with pyro.plate("data", x.shape[0]):
            z_loc = torch.zeros((x.shape[0], self.n_latent_features))
            z_scale = torch.ones((x.shape[0], self.n_latent_features))
            with pyro.poutine.scale(None, self.beta):
                z = pyro.sample("latent", dist.Normal(z_loc, z_scale).to_event(1))
            output = self.decoder.forward(z).view(-1, *self.input_shape)
            pyro.sample("obs", dist.Bernoulli(output).to_event(3), obs=x)

    def guide(self, x: torch.Tensor):
        """Pyro guide for VAE; q(z|x)"""
        pyro.module("encoder", self.encoder)
        with pyro.plate("data", x.shape[0]):
            z_loc, z_scale = self.encoder.forward(x.view(x.shape[0], -1))
            with pyro.poutine.scale(None, self.beta):
                pyro.sample("latent", dist.Normal(z_loc, z_scale).to_event(1))
In [8]:
batch_size = 32
lr = 1e-3
epochs = 20

bvae_model = BetaVariationalAutoencoder(
    n_data_features=28 * 28,  # MNIST pixels
    n_encoder_hidden_features=128,  # chosen arbitrarily
    n_decoder_hidden_features=128,  # chosen arbitrarily
    n_latent_features=10,  # how many features will be used to represent input
    beta=3.  # should limit the number of valuable features
)

train_dataloader = DataLoader(
    train_dataset,
    batch_size=batch_size,
    shuffle=True,
    drop_last=True,
)
val_loader = DataLoader(
    val_dataset, batch_size=batch_size, shuffle=False, drop_last=False
)

loss_fn = pyro.infer.Trace_ELBO().differentiable_loss
loss_fn_args = (bvae_model.model, bvae_model.guide)

train_ae(
    bvae_model,
    epochs=epochs,
    train_loader=train_dataloader,
    val_loader=val_loader,
    lr=lr,
    loss_fn=loss_fn,
    loss_fn_args=loss_fn_args,
)

Out[8]:
BetaVariationalAutoencoder(
  (encoder): VEncoder(
    (input_to_hidden): Linear(in_features=784, out_features=128, bias=True)
    (locs): Linear(in_features=128, out_features=10, bias=True)
    (scales): Linear(in_features=128, out_features=10, bias=True)
  )
  (decoder): VDecoder(
    (latent_to_hidden): Linear(in_features=10, out_features=128, bias=True)
    (hidden_to_output): Linear(in_features=128, out_features=784, bias=True)
  )
)
In [9]:
analyzer = AutoEncoderAnalyzer(model=bvae_model, dataset=val_dataset, n_samplings=5)
In [10]:
analyzer.compare_reconstruction_with_original()
plt.show()
In [11]:
analyzer.compare_samplings()
plt.show()
In [12]:
analyzer.average_points_per_class()
plt.show()
In [13]:
for digit, latent_code in enumerate(analyzer._averages):
    print(f"Digit: {digit}")
    analyzer.analyze_features(latent_code, steps=11)
    plt.show()
Digit: 0
Researching values in range [-5.0, 5.0]
Digit: 1
Researching values in range [-5.0, 5.0]
Digit: 2
Researching values in range [-5.0, 5.0]
Digit: 3
Researching values in range [-5.0, 5.0]
Digit: 4
Researching values in range [-5.0, 5.0]
Digit: 5
Researching values in range [-5.0, 5.0]
Digit: 6
Researching values in range [-5.0, 5.0]
Digit: 7
Researching values in range [-5.0, 5.0]
Digit: 8
Researching values in range [-5.0, 5.0]
Digit: 9
Researching values in range [-5.0, 5.0]
In [14]:
analyzer.analyze_tsne()  # this may take quite a long time
plt.show()
In [15]:
batch_size = 32
lr = 1e-3
epochs = 20

bvae_model = BetaVariationalAutoencoder(
    n_data_features=28 * 28,  # MNIST pixels
    n_encoder_hidden_features=128,  # chosen arbitrarily
    n_decoder_hidden_features=128,  # chosen arbitrarily
    n_latent_features=10,  # how many features will be used to represent input
    beta=8.  # should limit the number of valuable features
)

train_dataloader = DataLoader(
    train_dataset,
    batch_size=batch_size,
    shuffle=True,
    drop_last=True,
)
val_loader = DataLoader(
    val_dataset, batch_size=batch_size, shuffle=False, drop_last=False
)

loss_fn = pyro.infer.Trace_ELBO().differentiable_loss
loss_fn_args = (bvae_model.model, bvae_model.guide)

train_ae(
    bvae_model,
    epochs=epochs,
    train_loader=train_dataloader,
    val_loader=val_loader,
    lr=lr,
    loss_fn=loss_fn,
    loss_fn_args=loss_fn_args,
)

Out[15]:
BetaVariationalAutoencoder(
  (encoder): VEncoder(
    (input_to_hidden): Linear(in_features=784, out_features=128, bias=True)
    (locs): Linear(in_features=128, out_features=10, bias=True)
    (scales): Linear(in_features=128, out_features=10, bias=True)
  )
  (decoder): VDecoder(
    (latent_to_hidden): Linear(in_features=10, out_features=128, bias=True)
    (hidden_to_output): Linear(in_features=128, out_features=784, bias=True)
  )
)
In [16]:
analyzer = AutoEncoderAnalyzer(model=bvae_model, dataset=val_dataset, n_samplings=5)
In [17]:
analyzer.compare_reconstruction_with_original()
plt.show()
In [18]:
analyzer.compare_samplings()
plt.show()
In [19]:
analyzer.average_points_per_class()
plt.show()
In [20]:
analyzer.analyze_tsne()  # this may take quite a long time
plt.show()
In [21]:
for digit, latent_code in enumerate(analyzer._averages):
    print(f"Digit: {digit}")
    analyzer.analyze_features(latent_code, steps=11)
    plt.show()
Digit: 0
Researching values in range [-5.0, 5.0]
Digit: 1
Researching values in range [-5.0, 5.0]
Digit: 2
Researching values in range [-5.0, 5.0]
Digit: 3
Researching values in range [-5.0, 5.0]
Digit: 4
Researching values in range [-5.0, 5.0]
Digit: 5
Researching values in range [-5.0, 5.0]
Digit: 6
Researching values in range [-5.0, 5.0]
Digit: 7
Researching values in range [-5.0, 5.0]
Digit: 8
Researching values in range [-5.0, 5.0]
Digit: 9
Researching values in range [-5.0, 5.0]
In [ ]: